﻿//  --------------------------------
//  Copyright (c) AW Systems, Inc. All rights reserved.
//  This source code is made available under the terms of the Microsoft Public License (Ms-PL)
//  http://www.codeplex.com/SLAM/license
//  ---------------------------------

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using Slam.Configuration;
using Slam.Data;
using Slam.Events;
using Slam.Logging;
using Slam.SharePoint;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;

namespace Slam.Activation
{
	/// <summary>
	/// The SqlDataSynchronizeTypeProcessor consists of a method to be used by SLAM's IDataSynchronizer
	/// Synchronize method in which each type managed by SLAM is synchronized by using each type's
	/// IDataSynchronizerTypeProcessor.
	/// 
	/// SqlDataSynchronizeTypeProcessor is the default implementation of IDataSynchronizeTypeProcessor.
	/// Process expects to receive parameters specifying whether it should create database tables and whether
	/// it should populate those tables with data from SharePoint.
	/// </summary>
	public class SqlDataSynchronizeTypeProcessor : IDataSynchronizeTypeProcessor
	{
		#region Private State

		/// <summary>
		/// Internal repository/data access layer 
		/// </summary>
		private SqlDataRepository _database;

		/// <summary>
		/// SLAM Configuration
		/// </summary>
		private IConfigurationManager _configuration;

		/// <summary>
		/// SLAM Logger
		/// </summary>
		private ILogger _logger;

		#endregion

		#region Constructors

		/// <summary>
		/// Creates instance of SqlDataSynchronizeTypeProcessor for the given Configuration and Logger.
		/// </summary>
		/// <param name="configuration"></param>
		/// <param name="logger"></param>
		public SqlDataSynchronizeTypeProcessor(IConfigurationManager configuration, ILogger logger)
		{
			_configuration = configuration;
			string connectionString = "";
			if (_configuration.DataMapping is XmlSqlDataMapping)
			{
				connectionString = ((XmlSqlDataMapping)_configuration.DataMapping).GetConnectionString();
			}

			if (String.IsNullOrEmpty(connectionString))
			{
				connectionString = _configuration.GetSection<ConnectionStringSection>("ConnectionStrings").Default;
			}
			else
			{
				connectionString = _configuration.GetSection<ConnectionStringSection>("ConnectionStrings").GetConnectionString(connectionString);
			}

			_database = new SqlDataRepository(connectionString);
			_logger = logger;
		}

		/// <summary>
		/// Disposes of database connection on object destruction.
		/// </summary>
		~SqlDataSynchronizeTypeProcessor()
		{
			_database.Dispose();
		}

		#endregion

		#region IDataSynchronizeTypeProcessor Members

		/// <summary>
		/// Performs the synchronization process for the passed in ConfiguredType.  Process has two essential modes, 
		/// synchronize with cleanup, synchronize without cleanup.
		/// 
		/// Synchronizing a particular type consists of the following steps:
		/// 1. Create Item, Attachment, and Association Tables
		/// 2. Populate Data - If Content Type, iterate through all Lists in all sites of the site collection
		/// and calls IItemUpdater.Update on each List Item in each List that contains the Content Type for this type.
		/// If it is a List, calls IItemUpdater.Update on each List Item.
		/// 3. Cleanup tables by deleting non-synchronized items
		/// </summary>
		/// <param name="sharepointSiteLocation">Full URL location for the SharePoint Site to synchronize</param>
		/// <param name="parameters">A variable set of named parameters.  Expects at least a parameter called
		/// "ConfigType" containing the ConfiguredType to be processed, "CreateTable" indicating whether
		/// Process will creates the item and association tables for the given type, and "PopulateData"
		/// indicating whether data from SharePoint will be populated in the type's item and 
		/// association tables.</param>
		public void Process(string sharepointSiteLocation, IDictionary<string, object> parameters)
		{
			//parameters must contain a "ConfigType" to process
			if (parameters["ConfigType"] is ConfiguredType)
			{
				ConfiguredType inputType = (ConfiguredType)parameters["ConfigType"];
				_logger.LogMessage("Information", "Processing " + inputType.Name);

				SPSite site = new SPSite(sharepointSiteLocation);
				string siteCollection = site.ServerRelativeUrl.TrimStart('/');
				DataMapping currentDataMapping = _configuration.DataMapping;
				if (!String.IsNullOrEmpty(siteCollection) && _configuration.DataMappings.ContainsKey(siteCollection))
				{
					currentDataMapping = _configuration.DataMappings[siteCollection];
				}

				//Recreate SqlDataRepository based on the Current DataMapping in case the connection string is not the default
				string connectionString = "";
				if (currentDataMapping is XmlSqlDataMapping)
				{
					connectionString = ((XmlSqlDataMapping)currentDataMapping).GetConnectionString();
				}

				if (String.IsNullOrEmpty(connectionString))
				{
					connectionString = _configuration.GetSection<ConnectionStringSection>("ConnectionStrings").Default;
				}
				else
				{
					connectionString = _configuration.GetSection<ConnectionStringSection>("ConnectionStrings").GetConnectionString(connectionString);
				}

				_database = new SqlDataRepository(connectionString);

				//Get DataSchema from DataMapping
				string schemaName = ((XmlSqlDataMapping)currentDataMapping).GetDataSchema();

				_database.ResetVerifiedTableList();
				_database.EnsureDatabaseExists();

				if (!_database.VerifySchemaExists(schemaName))
				{
					_database.CreateSchema(schemaName);
				}

				string itemTypeName = inputType.Name;

				//For SqlConfiguredTypes the type name to use for getting type
				//information from the configuration should be the Table name if present
				//otherwise it should be the regular type Name.
				if (inputType is SqlConfiguredType)
				{
					SqlConfiguredType sqlType = (SqlConfiguredType)inputType;
					if (!String.IsNullOrEmpty(sqlType.Table))
					{
						itemTypeName = sqlType.Table;
					}
				}

				string tableName = GetTableName(itemTypeName);
				
				//If the location tables don't exist, create them
				if (!_database.VerifyTableExists("SLAM", "List") && parameters.ContainsKey("CreateTable") && (bool)parameters["CreateTable"])
				{
					_database.CreateLocationTables();
				}

				//The "ID" column used is always going to be the first Field returned by GetTypeFields
				Field idField = currentDataMapping.GetTypeFields(inputType.Id, inputType.Name).ToList()[0];
				string idColumnName = idField.Name;
				string idColumnSqlType = "";
				if (idField is SqlMappedField)
				{
					idColumnName = String.IsNullOrEmpty(((SqlMappedField)idField).Column) ? idColumnName : ((SqlMappedField)idField).Column;
					idColumnSqlType = String.IsNullOrEmpty(((SqlMappedField)idField).SqlType) ? idColumnName : ((SqlMappedField)idField).SqlType;
				}

				//If the Item table doesn't exist for this type and the CreateTable parameter is true, create it.
				if (!_database.VerifyTableExists(schemaName, tableName) && parameters.ContainsKey("CreateTable") && (bool)parameters["CreateTable"])
				{
					_logger.LogMessage("Information", "Create table for " + tableName);
					_database.CreateContentTable(schemaName, tableName, currentDataMapping.GetTypeFields(inputType.Id, inputType.Name) as IEnumerable<Field>);
				}
				else if (parameters.ContainsKey("CreateTable") && (bool)parameters["CreateTable"] && parameters.ContainsKey("PopulateData") && (bool)parameters["PopulateData"])
				{
					_logger.LogMessage("Information", "Prep table for synchronization: " + tableName);

					_database.AddTableSynchronizationObjects(schemaName, tableName, idColumnName);
				}

				_database.UpdateContentTable(schemaName, tableName, currentDataMapping.GetTypeFields(inputType.Id, inputType.Name) as IEnumerable<Field>);

				string attachmentTable = currentDataMapping.GetTypeAttribute(inputType.Id, inputType.Name, "AttachmentTableName");

				//If the Attachment table doesn't exist for this type and the CreateTable parameter is true, create it.
				if (!String.IsNullOrEmpty(attachmentTable) && !_database.VerifyTableExists(schemaName, attachmentTable) && parameters.ContainsKey("CreateTable") && (bool)parameters["CreateTable"])
				{
					_logger.LogMessage("Information", "Create table for " + attachmentTable);
					_database.CreateAttachmentTable(schemaName, attachmentTable, tableName, idColumnName, idColumnSqlType);
				}
				else if (!String.IsNullOrEmpty(attachmentTable) && parameters.ContainsKey("CreateTable") && (bool)parameters["CreateTable"] && parameters.ContainsKey("PopulateData") && (bool)parameters["PopulateData"])
				{
					_logger.LogMessage("Information", "Prep table for synchronization: " + attachmentTable);
					_database.AddAttachmentTableSynchronizationObjects(schemaName, attachmentTable, idColumnName);
				}

				//Check each Association for the type being processed
				foreach (AssociationField association in currentDataMapping.GetTypeAssociations(inputType.Id, inputType.Name))
				{
					if (association is SqlMappedAssociationField && !String.IsNullOrEmpty(association.AssociationName) && !String.IsNullOrEmpty(association.AssociatedTypeName))
					{
						SqlMappedAssociationField sqlField = (SqlMappedAssociationField)association;
						//If this Association table doesn't exist and the CreateTable parameter is true, create it.
						if (!_database.VerifyTableExists(schemaName, GetTableName(association.AssociationName)) && parameters.ContainsKey("CreateTable") && (bool)parameters["CreateTable"])
						{
							_logger.LogMessage("Information", "Create table for " + association.AssociationName);
							_database.CreateAssociationTable(schemaName, GetTableName(association.AssociationName), sqlField.FirstAssociatedTypeTableName, sqlField.FirstAssociatedTypeIdColumn, sqlField.FirstAssociatedTypeIdSqlType, sqlField.FirstAssociatedTypeIdSourceColumn, sqlField.SecondAssociatedTypeTableName, sqlField.SecondAssociatedTypeIdColumn, sqlField.SecondAssociatedTypeIdSqlType, sqlField.SecondAssociatedTypeIdSourceColumn);
						}
						else if (parameters.ContainsKey("PopulateData") && (bool)parameters["PopulateData"])
						{
							_logger.LogMessage("Information", "Prep association table for synchronization: " + association.AssociationName);
							_database.AddAssociationTableSynchronizationObjects(schemaName, GetTableName(association.AssociationName), sqlField.FirstAssociatedTypeIdColumn, sqlField.SecondAssociatedTypeIdColumn);
						}
					}
				}

				bool clearItemsOnSynchFinish = true;
				bool cleanupSynchObjects = parameters.ContainsKey("PopulateDataCleanup") && (bool)parameters["PopulateDataCleanup"];

				try
				{
					//Populate tables with data from SharePoint if the PopulateData parameter is true
					if (parameters.ContainsKey("PopulateData") && (bool)parameters["PopulateData"])
					{
						_logger.LogMessage("Information", "Populating data for " + tableName);
						using (SPSite outerSiteCollection = new SPSite(sharepointSiteLocation))
						{
							SPWeb currentWeb = null;

							try
							{
								currentWeb = outerSiteCollection.OpenWeb();
							}
							catch
							{
								//failed
							}

							if (currentWeb != null)
							{
								SPSecurity.RunWithElevatedPrivileges(delegate()
								{
									using (SPSite elevatedSite = new SPSite(currentWeb.Site.ID))
									{
										SPWeb elevatedWeb = null;
										try
										{
											//If Site is empty, open the Root Web
											if (String.IsNullOrEmpty(inputType.Site))
											{
												elevatedWeb = elevatedSite.RootWeb;
											}
											else
											{
												elevatedWeb = elevatedSite.AllWebs[inputType.Site];
											}
										}
										catch
										{
											//failed
										}

										if (elevatedWeb != null && elevatedWeb.Exists)
										{
											_logger.LogMessage("Information", "Populating data for " + tableName + " from site");

											if (!inputType.ContentType)
											{
												try
												{
													SPList list = null;


													//If ID is present, try to open the List by ID otherwise just
													//let it fail...
													if (!String.IsNullOrEmpty(inputType.Id))
													{
														list = elevatedWeb.Lists[new Guid(inputType.Id)];
													}
													else
													{
														list = elevatedWeb.Lists[inputType.Name];
													}

													if (list != null)
													{
														_logger.LogMessage("Information", "Updating data from List: " + list.RootFolder.ServerRelativeUrl + " for " + tableName);

														SynchronizeList(list, null);
														SlamItemContext.ResetContext();
													}
												}
												catch (Exception exception)
												{
													_logger.LogMessage("Exception", exception.ToString());
												}

												//Check is the special AllSites option is set to true.  
												//If a type includes an "AllSites" attribute value that is set to
												//true event handlers are attached to ALL Lists with the type Name.
												//Therefore to process a type with AllSites specified as true
												//all of the other lists after the first above need processed.
												if (!String.IsNullOrEmpty(currentDataMapping.GetTypeAttribute(inputType.Id, inputType.Name, "AllSites")) && currentDataMapping.GetTypeAttribute(inputType.Id, inputType.Name, "AllSites").ToLower() == "true")
												{
													try
													{
														for (int i = 0; i < elevatedSite.AllWebs.Count; i++)
														{
															SPWeb subWeb = elevatedSite.AllWebs[i];

															SPList list = null;

															//If ID is present, try to open the List by ID otherwise just
															//let it fail...
															if (!String.IsNullOrEmpty(inputType.Id))
															{
																list = subWeb.Lists[new Guid(inputType.Id)];
															}
															else
															{
																list = subWeb.Lists[inputType.Name];
															}

															if (list != null)
															{
																_logger.LogMessage("Information", "Updating data from List: " + list.RootFolder.ServerRelativeUrl + " for " + tableName);

																SynchronizeList(list, null);
																SlamItemContext.ResetContext();
															}

															subWeb.Dispose();
														}
													}
													catch (Exception exception)
													{
														_logger.LogMessage("Exception", exception.ToString());
													}
												}
											}
											else //If type is a ContentType
											{

												SPContentType contentType = null;
												try
												{
													contentType = elevatedWeb.ContentTypes[inputType.Name];
												}
												catch (Exception exception)
												{
													_logger.LogMessage("Exception", exception.ToString());
												}

												if (contentType != null)
												{
													try
													{
														//To process a Content Type, iterate through
														//all sites in the site collection, find each
														//List that contains this Content Type and call
														//IItemUpdater.Update on every item that is of this
														//Content Type.
														for (int i = 0; i < elevatedSite.AllWebs.Count; i++)
														{
															try
															{
																SPWeb subWeb = elevatedSite.AllWebs[i];
																foreach (SPList list in subWeb.Lists)
																{
																	if (list.ContentTypes.Contains(contentType))
																	{
																		_logger.LogMessage("Information", "Updating data of Content Type: " + contentType.Name + " from List: " + list.RootFolder.ServerRelativeUrl + " for " + tableName);

																		SynchronizeList(list, contentType);
																		SlamItemContext.ResetContext();
																	}
																}
																subWeb.Dispose();
															}
															catch (Exception exception)
															{
																_logger.LogMessage("Exception", exception.ToString());
															}
														}
													}
													catch (Exception exception)
													{
														_logger.LogMessage("Exception", exception.ToString());
													}
												}
											}
											elevatedWeb.Dispose();
										}
									}
								});

								currentWeb.Dispose();
							}
						}
					}

					site.Dispose();
				}
				catch (Exception exception)
				{
					_logger.LogMessage("Exception", exception.ToString());
					clearItemsOnSynchFinish = false;
				}
				finally
				{
					if (cleanupSynchObjects)
					{	
						int deleted = _database.DropTableSynchronizationObjects(schemaName, tableName, clearItemsOnSynchFinish);
						_logger.LogMessage("Information", "Number of items removed from " + tableName + ": " + deleted.ToString());

						if (!String.IsNullOrEmpty(attachmentTable))
						{
							deleted = _database.DropTableSynchronizationObjects(schemaName, attachmentTable, clearItemsOnSynchFinish);
							_logger.LogMessage("Information", "Number of items removed from " + attachmentTable + ": " + deleted.ToString());
						}

						foreach (AssociationField association in currentDataMapping.GetTypeAssociations(inputType.Id, inputType.Name))
						{
							if (!String.IsNullOrEmpty(association.AssociationName))
							{
								deleted = _database.DropTableSynchronizationObjects(schemaName, GetTableName(association.AssociationName), clearItemsOnSynchFinish);
								_logger.LogMessage("Information", "Number of items removed from " + GetTableName(association.AssociationName) + ": " + deleted.ToString());
							}
						}
					}
				}
			}
		}

		#endregion

		#region Private Members

		/// <summary>
		/// Helper method that strips the given type name of non-word characters and
		/// spaces to ensure a well-formed SQL table name.
		/// </summary>
		/// <param name="typeName"></param>
		/// <returns></returns>
		private string GetTableName(string typeName)
		{
			return Regex.Replace(typeName, "[\\W\\s]", "");
		}

		/// <summary>
		/// Helper method for paged synchronization of list items.
		/// </summary>
		/// <param name="list">This is the current List object</param>      
		private void SynchronizeList(SPList list, SPContentType contentType)
		{
			SPQuery query = new SPQuery();
			query.RowLimit = 1000;
			query.ViewAttributes = "Scope=\"RecursiveAll\"";
			if (contentType != null)
				query.Query = @"<Where><BeginsWith><FieldRef Name=""ContentTypeId"" /><Value Type=""ContentTypeId"">" + contentType.Id.ToString() + @"</Value></BeginsWith></Where>";

			do
			{
				SPListItemCollection listItems = list.GetItems(query);

				foreach (SPListItem listItem in listItems)
				{
					SynchronizeListItem(listItem);
				}
				query.ListItemCollectionPosition = listItems.ListItemCollectionPosition;
			} while (query.ListItemCollectionPosition != null);
		}

		/// <summary>
		/// Helper method that gets ensures a SLAM ID is assigned if SLAM ID is present and performs SlamUpdate on the given list item.
		/// </summary>
		/// <param name="listItem">List Item to slam</param>
		private void SynchronizeListItem(SPListItem listItem)
		{
			string id = Convert.ToString(listItem.GetUniqueId(false, false));
			if (listItem.Fields.ContainsField("SLAM ID") && String.IsNullOrEmpty(id))
			{
				listItem["SLAM ID"] = listItem.GetUniqueId(true, false);
				listItem.SystemUpdate(false);
			}
			listItem.SlamUpdate();
		}


		#endregion
	}
}

